#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
网络文件处理模块

提供对网络文件的下载、上传、FTP操作等功能
"""

import os
import re
import tempfile
import time
from pathlib import Path
from typing import Union, List, Dict, Any, Optional, Tuple, Iterator, Set, BinaryIO, Callable

# 导入异常处理装饰器
from ..exception.exception_handler import exception_handler
from ..exception.exceptions import FileException, NetworkException

# 导入日志模块
from ..logger.logger import Logger

# 创建日志记录器
logger = Logger("network_file_handler").get_logger()


class HTTPFileHandler:
    """
    HTTP文件处理类
    
    提供对HTTP/HTTPS文件的下载、上传等功能
    
    注意：此类需要安装requests库
    pip install requests
    """
    
    def __init__(self):
        """
        初始化HTTP文件处理器
        """
        try:
            import requests
            self.requests = requests
        except ImportError as e:
            logger.error(f"初始化HTTP文件处理器失败: {e}")
            logger.error("请安装必要的依赖: pip install requests")
            raise ImportError("请安装必要的依赖: pip install requests") from e
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def download_file(self, url: str, output_path: Union[str, Path], 
                    headers: Optional[Dict[str, str]] = None,
                    params: Optional[Dict[str, str]] = None,
                    auth: Optional[Tuple[str, str]] = None,
                    timeout: int = 30,
                    verify: bool = True,
                    progress_callback: Optional[Callable[[int, int], None]] = None) -> str:
        """
        下载文件
        
        Args:
            url: 文件URL
            output_path: 输出文件路径
            headers: 请求头，默认为None
            params: 请求参数，默认为None
            auth: 认证信息（用户名，密码），默认为None
            timeout: 超时时间（秒），默认为30
            verify: 是否验证SSL证书，默认为True
            progress_callback: 进度回调函数，接收已下载大小和总大小两个参数，默认为None
            
        Returns:
            下载的文件路径
            
        Raises:
            NetworkException: 下载失败时抛出
            
        Examples:
            >>> handler = HTTPFileHandler()
            >>> # 简单下载
            >>> file_path = handler.download_file('https://example.com/file.zip', 'downloads/file.zip')
            >>> # 带进度回调的下载
            >>> def progress(downloaded, total):
            ...     percent = int(downloaded * 100 / total) if total else 0
            ...     print(f"下载进度: {percent}%")
            >>> file_path = handler.download_file('https://example.com/file.zip', 'downloads/file.zip', progress_callback=progress)
        """
        output_path = Path(output_path)
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 发起请求
            with self.requests.get(url, headers=headers, params=params, auth=auth, 
                                 timeout=timeout, verify=verify, stream=True) as response:
                # 检查响应状态
                response.raise_for_status()
                
                # 获取文件大小
                total_size = int(response.headers.get('content-length', 0))
                
                # 下载文件
                downloaded_size = 0
                with open(output_path, 'wb') as file:
                    for chunk in response.iter_content(chunk_size=8192):
                        if chunk:  # 过滤掉保持连接的空块
                            file.write(chunk)
                            downloaded_size += len(chunk)
                            
                            # 调用进度回调
                            if progress_callback:
                                progress_callback(downloaded_size, total_size)
            
            logger.info(f"下载文件成功: {url} -> {output_path}")
            return str(output_path)
        except self.requests.exceptions.RequestException as e:
            raise NetworkException(f"下载文件失败: {e}")
        except Exception as e:
            raise NetworkException(f"下载文件失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def upload_file(self, file_path: Union[str, Path], url: str, 
                  field_name: str = 'file',
                  headers: Optional[Dict[str, str]] = None,
                  data: Optional[Dict[str, str]] = None,
                  auth: Optional[Tuple[str, str]] = None,
                  timeout: int = 30,
                  verify: bool = True) -> Dict[str, Any]:
        """
        上传文件
        
        Args:
            file_path: 要上传的文件路径
            url: 上传URL
            field_name: 文件字段名，默认为'file'
            headers: 请求头，默认为None
            data: 附加表单数据，默认为None
            auth: 认证信息（用户名，密码），默认为None
            timeout: 超时时间（秒），默认为30
            verify: 是否验证SSL证书，默认为True
            
        Returns:
            服务器响应的JSON数据
            
        Raises:
            NetworkException: 上传失败时抛出
            
        Examples:
            >>> handler = HTTPFileHandler()
            >>> # 简单上传
            >>> response = handler.upload_file('example.txt', 'https://example.com/upload')
            >>> # 带附加数据的上传
            >>> data = {'description': '示例文件'}
            >>> response = handler.upload_file('example.txt', 'https://example.com/upload', data=data)
        """
        file_path = Path(file_path)
        
        # 检查文件是否存在
        if not file_path.exists():
            raise NetworkException(f"要上传的文件不存在: {file_path}")
        
        try:
            # 准备文件
            files = {field_name: (file_path.name, open(file_path, 'rb'))}
            
            # 发起请求
            response = self.requests.post(url, headers=headers, data=data, files=files, 
                                        auth=auth, timeout=timeout, verify=verify)
            
            # 检查响应状态
            response.raise_for_status()
            
            # 关闭文件
            files[field_name][1].close()
            
            # 尝试解析JSON响应
            try:
                result = response.json()
            except ValueError:
                result = {'status': 'success', 'text': response.text}
            
            logger.info(f"上传文件成功: {file_path} -> {url}")
            return result
        except self.requests.exceptions.RequestException as e:
            raise NetworkException(f"上传文件失败: {e}")
        except Exception as e:
            raise NetworkException(f"上传文件失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def get_file_info(self, url: str, headers: Optional[Dict[str, str]] = None,
                    timeout: int = 10, verify: bool = True) -> Dict[str, Any]:
        """
        获取远程文件信息
        
        Args:
            url: 文件URL
            headers: 请求头，默认为None
            timeout: 超时时间（秒），默认为10
            verify: 是否验证SSL证书，默认为True
            
        Returns:
            文件信息字典，包含大小、类型、修改时间等
            
        Raises:
            NetworkException: 获取失败时抛出
            
        Examples:
            >>> handler = HTTPFileHandler()
            >>> info = handler.get_file_info('https://example.com/file.zip')
            >>> print(f"文件大小: {info['size']}")
            >>> print(f"文件类型: {info['content_type']}")
        """
        try:
            # 发起HEAD请求
            response = self.requests.head(url, headers=headers, timeout=timeout, verify=verify)
            
            # 检查响应状态
            response.raise_for_status()
            
            # 提取文件信息
            headers = response.headers
            info = {
                'url': url,
                'size': int(headers.get('content-length', 0)),
                'content_type': headers.get('content-type', 'unknown'),
                'last_modified': headers.get('last-modified', None),
                'etag': headers.get('etag', None),
                'status_code': response.status_code
            }
            
            logger.info(f"获取文件信息成功: {url}")
            return info
        except self.requests.exceptions.RequestException as e:
            raise NetworkException(f"获取文件信息失败: {e}")
        except Exception as e:
            raise NetworkException(f"获取文件信息失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def download_multiple_files(self, urls: List[str], output_dir: Union[str, Path],
                             max_workers: int = 5, timeout: int = 30,
                             verify: bool = True) -> List[str]:
        """
        并行下载多个文件
        
        Args:
            urls: 文件URL列表
            output_dir: 输出目录
            max_workers: 最大并行下载数，默认为5
            timeout: 超时时间（秒），默认为30
            verify: 是否验证SSL证书，默认为True
            
        Returns:
            下载的文件路径列表
            
        Raises:
            NetworkException: 下载失败时抛出
            
        Examples:
            >>> handler = HTTPFileHandler()
            >>> urls = [
            ...     'https://example.com/file1.zip',
            ...     'https://example.com/file2.zip',
            ...     'https://example.com/file3.zip'
            ... ]
            >>> file_paths = handler.download_multiple_files(urls, 'downloads')
        """
        output_dir = Path(output_dir)
        
        # 确保输出目录存在
        output_dir.mkdir(parents=True, exist_ok=True)
        
        try:
            # 导入并发库
            try:
                from concurrent.futures import ThreadPoolExecutor
            except ImportError:
                # Python 3.2+已内置concurrent.futures
                pass
            
            # 定义下载单个文件的函数
            def download_single(url):
                try:
                    # 从URL中提取文件名
                    file_name = url.split('/')[-1].split('?')[0]
                    if not file_name:
                        file_name = f"download_{int(time.time())}_{hash(url) % 10000}"
                    
                    output_path = output_dir / file_name
                    return self.download_file(url, output_path, timeout=timeout, verify=verify)
                except Exception as e:
                    logger.error(f"下载文件失败: {url} - {e}")
                    return None
            
            # 并行下载文件
            downloaded_files = []
            with ThreadPoolExecutor(max_workers=max_workers) as executor:
                results = list(executor.map(download_single, urls))
                downloaded_files = [f for f in results if f]  # 过滤掉下载失败的文件
            
            logger.info(f"并行下载文件完成: 成功 {len(downloaded_files)}/{len(urls)}")
            return downloaded_files
        except Exception as e:
            raise NetworkException(f"并行下载文件失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def resume_download(self, url: str, output_path: Union[str, Path],
                      headers: Optional[Dict[str, str]] = None,
                      timeout: int = 30, verify: bool = True,
                      chunk_size: int = 8192,
                      progress_callback: Optional[Callable[[int, int], None]] = None) -> str:
        """
        断点续传下载文件
        
        Args:
            url: 文件URL
            output_path: 输出文件路径
            headers: 请求头，默认为None
            timeout: 超时时间（秒），默认为30
            verify: 是否验证SSL证书，默认为True
            chunk_size: 每次读取的块大小，默认为8192
            progress_callback: 进度回调函数，接收已下载大小和总大小两个参数，默认为None
            
        Returns:
            下载的文件路径
            
        Raises:
            NetworkException: 下载失败时抛出
            
        Examples:
            >>> handler = HTTPFileHandler()
            >>> # 断点续传下载
            >>> file_path = handler.resume_download('https://example.com/file.zip', 'downloads/file.zip')
        """
        output_path = Path(output_path)
        
        # 确保输出目录存在
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        # 初始化请求头
        if headers is None:
            headers = {}
        
        try:
            # 检查文件是否已存在
            file_size = 0
            if output_path.exists():
                file_size = output_path.stat().st_size
                headers['Range'] = f'bytes={file_size}-'
            
            # 发起请求
            with self.requests.get(url, headers=headers, timeout=timeout, verify=verify, stream=True) as response:
                # 检查响应状态
                if file_size > 0 and response.status_code == 416:  # 请求范围不满足
                    # 文件可能已经下载完成
                    logger.info(f"文件已经下载完成: {output_path}")
                    return str(output_path)
                
                response.raise_for_status()
                
                # 获取文件总大小
                if 'content-range' in response.headers:
                    content_range = response.headers['content-range']
                    total_size = int(content_range.split('/')[-1])
                else:
                    total_size = int(response.headers.get('content-length', 0)) + file_size
                
                # 下载文件
                mode = 'ab' if file_size > 0 else 'wb'
                downloaded_size = file_size
                
                with open(output_path, mode) as file:
                    for chunk in response.iter_content(chunk_size=chunk_size):
                        if chunk:  # 过滤掉保持连接的空块
                            file.write(chunk)
                            downloaded_size += len(chunk)
                            
                            # 调用进度回调
                            if progress_callback:
                                progress_callback(downloaded_size, total_size)
            
            logger.info(f"断点续传下载文件成功: {url} -> {output_path}")
            return str(output_path)
        except self.requests.exceptions.RequestException as e:
            raise NetworkException(f"断点续传下载文件失败: {e}")
        except Exception as e:
            raise NetworkException(f"断点续传下载文件失败: {e}")


class FTPHandler:
    """
    FTP文件处理类
    
    提供对FTP/FTPS服务器的文件上传、下载、列表等功能
    """
    
    def __init__(self):
        """
        初始化FTP处理器
        """
        try:
            import ftplib
            self.ftplib = ftplib
        except ImportError as e:
            logger.error(f"初始化FTP处理器失败: {e}")
            logger.error("ftplib模块是Python标准库的一部分，请确保Python安装正确")
            raise ImportError("ftplib模块是Python标准库的一部分，请确保Python安装正确") from e
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def connect(self, host: str, port: int = 21, user: str = 'anonymous', 
              password: str = '', use_tls: bool = False, timeout: int = 30) -> 'ftplib.FTP':
        """
        连接到FTP服务器
        
        Args:
            host: FTP服务器主机名或IP
            port: FTP服务器端口，默认为21
            user: 用户名，默认为'anonymous'
            password: 密码，默认为空
            use_tls: 是否使用FTPS（FTP over TLS），默认为False
            timeout: 超时时间（秒），默认为30
            
        Returns:
            FTP连接对象
            
        Raises:
            NetworkException: 连接失败时抛出
            
        Examples:
            >>> handler = FTPHandler()
            >>> # 匿名连接
            >>> ftp = handler.connect('ftp.example.com')
            >>> # 带用户名密码连接
            >>> ftp = handler.connect('ftp.example.com', user='user', password='pass')
            >>> # 使用FTPS连接
            >>> ftp = handler.connect('ftps.example.com', use_tls=True, user='user', password='pass')
            >>> # 使用完毕后关闭连接
            >>> ftp.quit()
        """
        try:
            # 创建FTP连接
            if use_tls:
                ftp = self.ftplib.FTP_TLS(timeout=timeout)
            else:
                ftp = self.ftplib.FTP(timeout=timeout)
            
            # 连接到服务器
            ftp.connect(host, port)
            
            # 登录
            ftp.login(user, password)
            
            # 如果使用TLS，启用数据连接保护
            if use_tls:
                ftp.prot_p()
            
            logger.info(f"连接到FTP服务器成功: {host}:{port}")
            return ftp
        except self.ftplib.all_errors as e:
            raise NetworkException(f"连接到FTP服务器失败: {e}")
        except Exception as e:
            raise NetworkException(f"连接到FTP服务器失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def list_files(self, ftp: 'ftplib.FTP', remote_dir: str = '.') -> List[Dict[str, Any]]:
        """
        列出FTP服务器上的文件和目录
        
        Args:
            ftp: FTP连接对象
            remote_dir: 远程目录，默认为当前目录
            
        Returns:
            文件和目录信息列表
            
        Raises:
            NetworkException: 列表失败时抛出
            
        Examples:
            >>> handler = FTPHandler()
            >>> ftp = handler.connect('ftp.example.com')
            >>> # 列出根目录文件
            >>> files = handler.list_files(ftp)
            >>> # 列出指定目录文件
            >>> files = handler.list_files(ftp, '/pub/files')
            >>> ftp.quit()
        """
        try:
            # 切换到指定目录
            ftp.cwd(remote_dir)
            
            # 获取文件列表
            files = []
            
            # 定义处理函数
            def process_line(line):
                parts = line.split(None, 8)
                if len(parts) >= 9:
                    permissions, _, owner, group, size, month, day, time_or_year, name = parts
                    
                    # 判断是否为目录
                    is_dir = permissions.startswith('d')
                    
                    files.append({
                        'name': name,
                        'is_dir': is_dir,
                        'size': int(size),
                        'permissions': permissions,
                        'owner': owner,
                        'group': group,
                        'month': month,
                        'day': day,
                        'time_or_year': time_or_year
                    })
            
            # 获取详细列表
            ftp.retrlines('LIST', process_line)
            
            logger.info(f"列出FTP目录成功: {remote_dir} (共 {len(files)} 个文件/目录)")
            return files
        except self.ftplib.all_errors as e:
            raise NetworkException(f"列出FTP目录失败: {e}")
        except Exception as e:
            raise NetworkException(f"列出FTP目录失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def download_file(self, ftp: 'ftplib.FTP', remote_path: str, local_path: Union[str, Path],
                    binary_mode: bool = True,
                    progress_callback: Optional[Callable[[int, int], None]] = None) -> str:
        """
        从FTP服务器下载文件
        
        Args:
            ftp: FTP连接对象
            remote_path: 远程文件路径
            local_path: 本地文件路径
            binary_mode: 是否使用二进制模式，默认为True
            progress_callback: 进度回调函数，接收已下载大小和总大小两个参数，默认为None
            
        Returns:
            下载的文件路径
            
        Raises:
            NetworkException: 下载失败时抛出
            
        Examples:
            >>> handler = FTPHandler()
            >>> ftp = handler.connect('ftp.example.com')
            >>> # 下载文件
            >>> file_path = handler.download_file(ftp, '/pub/file.zip', 'downloads/file.zip')
            >>> ftp.quit()
        """
        local_path = Path(local_path)
        
        # 确保输出目录存在
        local_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 获取文件大小
            ftp.voidcmd('TYPE I')  # 切换到二进制模式以获取准确的文件大小
            total_size = ftp.size(remote_path)
            
            # 打开本地文件
            with open(local_path, 'wb' if binary_mode else 'w') as local_file:
                # 定义回调函数
                downloaded_size = [0]  # 使用列表以便在回调中修改
                
                def callback(data):
                    local_file.write(data)
                    downloaded_size[0] += len(data)
                    if progress_callback:
                        progress_callback(downloaded_size[0], total_size)
                
                # 下载文件
                if binary_mode:
                    ftp.retrbinary(f'RETR {remote_path}', callback)
                else:
                    ftp.retrlines(f'RETR {remote_path}', callback)
            
            logger.info(f"从FTP下载文件成功: {remote_path} -> {local_path}")
            return str(local_path)
        except self.ftplib.all_errors as e:
            raise NetworkException(f"从FTP下载文件失败: {e}")
        except Exception as e:
            raise NetworkException(f"从FTP下载文件失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def upload_file(self, ftp: 'ftplib.FTP', local_path: Union[str, Path], remote_path: str,
                  binary_mode: bool = True,
                  progress_callback: Optional[Callable[[int, int], None]] = None) -> None:
        """
        上传文件到FTP服务器
        
        Args:
            ftp: FTP连接对象
            local_path: 本地文件路径
            remote_path: 远程文件路径
            binary_mode: 是否使用二进制模式，默认为True
            progress_callback: 进度回调函数，接收已上传大小和总大小两个参数，默认为None
            
        Raises:
            NetworkException: 上传失败时抛出
            
        Examples:
            >>> handler = FTPHandler()
            >>> ftp = handler.connect('ftp.example.com', user='user', password='pass')
            >>> # 上传文件
            >>> handler.upload_file(ftp, 'example.txt', '/uploads/example.txt')
            >>> ftp.quit()
        """
        local_path = Path(local_path)
        
        # 检查本地文件是否存在
        if not local_path.exists():
            raise NetworkException(f"要上传的文件不存在: {local_path}")
        
        try:
            # 获取文件大小
            total_size = local_path.stat().st_size
            
            # 打开本地文件
            with open(local_path, 'rb' if binary_mode else 'r') as local_file:
                # 定义回调函数
                if progress_callback:
                    uploaded_size = [0]  # 使用列表以便在回调中修改
                    
                    def callback(block):
                        uploaded_size[0] += len(block)
                        progress_callback(uploaded_size[0], total_size)
                    
                    # 上传文件
                    if binary_mode:
                        ftp.storbinary(f'STOR {remote_path}', local_file, callback=callback)
                    else:
                        ftp.storlines(f'STOR {remote_path}', local_file, callback=callback)
                else:
                    # 上传文件（无进度回调）
                    if binary_mode:
                        ftp.storbinary(f'STOR {remote_path}', local_file)
                    else:
                        ftp.storlines(f'STOR {remote_path}', local_file)
            
            logger.info(f"上传文件到FTP成功: {local_path} -> {remote_path}")
        except self.ftplib.all_errors as e:
            raise NetworkException(f"上传文件到FTP失败: {e}")
        except Exception as e:
            raise NetworkException(f"上传文件到FTP失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def create_directory(self, ftp: 'ftplib.FTP', remote_dir: str) -> None:
        """
        在FTP服务器上创建目录
        
        Args:
            ftp: FTP连接对象
            remote_dir: 远程目录路径
            
        Raises:
            NetworkException: 创建失败时抛出
            
        Examples:
            >>> handler = FTPHandler()
            >>> ftp = handler.connect('ftp.example.com', user='user', password='pass')
            >>> # 创建目录
            >>> handler.create_directory(ftp, '/uploads/new_folder')
            >>> ftp.quit()
        """
        try:
            # 尝试创建目录
            ftp.mkd(remote_dir)
            
            logger.info(f"在FTP服务器上创建目录成功: {remote_dir}")
        except self.ftplib.error_perm as e:
            # 检查是否是因为目录已存在
            if str(e).startswith('550'):
                logger.warning(f"FTP目录已存在: {remote_dir}")
            else:
                raise NetworkException(f"在FTP服务器上创建目录失败: {e}")
        except Exception as e:
            raise NetworkException(f"在FTP服务器上创建目录失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def delete_file(self, ftp: 'ftplib.FTP', remote_path: str) -> None:
        """
        删除FTP服务器上的文件
        
        Args:
            ftp: FTP连接对象
            remote_path: 远程文件路径
            
        Raises:
            NetworkException: 删除失败时抛出
            
        Examples:
            >>> handler = FTPHandler()
            >>> ftp = handler.connect('ftp.example.com', user='user', password='pass')
            >>> # 删除文件
            >>> handler.delete_file(ftp, '/uploads/example.txt')
            >>> ftp.quit()
        """
        try:
            # 删除文件
            ftp.delete(remote_path)
            
            logger.info(f"删除FTP服务器上的文件成功: {remote_path}")
        except self.ftplib.all_errors as e:
            raise NetworkException(f"删除FTP服务器上的文件失败: {e}")
        except Exception as e:
            raise NetworkException(f"删除FTP服务器上的文件失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def rename_file(self, ftp: 'ftplib.FTP', from_path: str, to_path: str) -> None:
        """
        重命名FTP服务器上的文件或目录
        
        Args:
            ftp: FTP连接对象
            from_path: 原路径
            to_path: 新路径
            
        Raises:
            NetworkException: 重命名失败时抛出
            
        Examples:
            >>> handler = FTPHandler()
            >>> ftp = handler.connect('ftp.example.com', user='user', password='pass')
            >>> # 重命名文件
            >>> handler.rename_file(ftp, '/uploads/old.txt', '/uploads/new.txt')
            >>> ftp.quit()
        """
        try:
            # 重命名文件或目录
            ftp.rename(from_path, to_path)
            
            logger.info(f"重命名FTP服务器上的文件/目录成功: {from_path} -> {to_path}")
        except self.ftplib.all_errors as e:
            raise NetworkException(f"重命名FTP服务器上的文件/目录失败: {e}")
        except Exception as e:
            raise NetworkException(f"重命名FTP服务器上的文件/目录失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def download_directory(self, ftp: 'ftplib.FTP', remote_dir: str, local_dir: Union[str, Path],
                        binary_mode: bool = True) -> List[str]:
        """
        递归下载FTP服务器上的目录
        
        Args:
            ftp: FTP连接对象
            remote_dir: 远程目录路径
            local_dir: 本地目录路径
            binary_mode: 是否使用二进制模式，默认为True
            
        Returns:
            下载的文件路径列表
            
        Raises:
            NetworkException: 下载失败时抛出
            
        Examples:
            >>> handler = FTPHandler()
            >>> ftp = handler.connect('ftp.example.com')
            >>> # 下载整个目录
            >>> files = handler.download_directory(ftp, '/pub/files', 'downloads/files')
            >>> ftp.quit()
        """
        local_dir = Path(local_dir)
        
        # 确保本地目录存在
        local_dir.mkdir(parents=True, exist_ok=True)
        
        try:
            # 保存当前工作目录
            original_dir = ftp.pwd()
            
            # 切换到远程目录
            try:
                ftp.cwd(remote_dir)
            except self.ftplib.error_perm as e:
                raise NetworkException(f"远程目录不存在或无法访问: {remote_dir} - {e}")
            
            # 获取文件列表
            files = self.list_files(ftp)
            
            # 下载文件和子目录
            downloaded_files = []
            
            for item in files:
                name = item['name']
                # 跳过当前目录和父目录
                if name in ('.', '..'):
                    continue
                
                remote_path = f"{remote_dir}/{name}"
                local_path = local_dir / name
                
                if item['is_dir']:
                    # 递归下载子目录
                    sub_files = self.download_directory(ftp, remote_path, local_path, binary_mode)
                    downloaded_files.extend(sub_files)
                else:
                    # 下载文件
                    downloaded_file = self.download_file(ftp, name, local_path, binary_mode)
                    downloaded_files.append(downloaded_file)
            
            # 恢复原始工作目录
            ftp.cwd(original_dir)
            
            logger.info(f"从FTP下载目录成功: {remote_dir} -> {local_dir} (共 {len(downloaded_files)} 个文件)")
            return downloaded_files
        except NetworkException:
            # 重新抛出已经处理过的异常
            raise
        except self.ftplib.all_errors as e:
            raise NetworkException(f"从FTP下载目录失败: {e}")
        except Exception as e:
            raise NetworkException(f"从FTP下载目录失败: {e}")


class SFTPHandler:
    """
    SFTP文件处理类
    
    提供对SFTP服务器的文件上传、下载、列表等功能
    
    注意：此类需要安装paramiko库
    pip install paramiko
    """
    
    def __init__(self):
        """
        初始化SFTP处理器
        """
        try:
            import paramiko
            self.paramiko = paramiko
        except ImportError as e:
            logger.error(f"初始化SFTP处理器失败: {e}")
            logger.error("请安装必要的依赖: pip install paramiko")
            raise ImportError("请安装必要的依赖: pip install paramiko") from e
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def connect(self, host: str, port: int = 22, user: str = None, password: str = None,
              key_filename: Optional[str] = None, timeout: int = 30) -> Tuple['paramiko.SSHClient', 'paramiko.SFTPClient']:
        """
        连接到SFTP服务器
        
        Args:
            host: SFTP服务器主机名或IP
            port: SFTP服务器端口，默认为22
            user: 用户名
            password: 密码，与key_filename至少提供一个
            key_filename: 私钥文件路径，与password至少提供一个
            timeout: 超时时间（秒），默认为30
            
        Returns:
            SSH客户端和SFTP客户端的元组
            
        Raises:
            NetworkException: 连接失败时抛出
            
        Examples:
            >>> handler = SFTPHandler()
            >>> # 使用密码连接
            >>> ssh, sftp = handler.connect('sftp.example.com', user='user', password='pass')
            >>> # 使用密钥连接
            >>> ssh, sftp = handler.connect('sftp.example.com', user='user', key_filename='~/.ssh/id_rsa')
            >>> # 使用完毕后关闭连接
            >>> sftp.close()
            >>> ssh.close()
        """
        try:
            # 创建SSH客户端
            ssh = self.paramiko.SSHClient()
            ssh.set_missing_host_key_policy(self.paramiko.AutoAddPolicy())
            
            # 连接到服务器
            connect_kwargs = {
                'hostname': host,
                'port': port,
                'timeout': timeout
            }
            
            if user:
                connect_kwargs['username'] = user
            
            if password:
                connect_kwargs['password'] = password
            
            if key_filename:
                connect_kwargs['key_filename'] = key_filename
            
            ssh.connect(**connect_kwargs)
            
            # 创建SFTP客户端
            sftp = ssh.open_sftp()
            
            logger.info(f"连接到SFTP服务器成功: {host}:{port}")
            return ssh, sftp
        except self.paramiko.SSHException as e:
            raise NetworkException(f"连接到SFTP服务器失败: {e}")
        except Exception as e:
            raise NetworkException(f"连接到SFTP服务器失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def list_files(self, sftp: 'paramiko.SFTPClient', remote_dir: str = '.') -> List[Dict[str, Any]]:
        """
        列出SFTP服务器上的文件和目录
        
        Args:
            sftp: SFTP客户端
            remote_dir: 远程目录，默认为当前目录
            
        Returns:
            文件和目录信息列表
            
        Raises:
            NetworkException: 列表失败时抛出
            
        Examples:
            >>> handler = SFTPHandler()
            >>> ssh, sftp = handler.connect('sftp.example.com', user='user', password='pass')
            >>> # 列出根目录文件
            >>> files = handler.list_files(sftp)
            >>> # 列出指定目录文件
            >>> files = handler.list_files(sftp, '/home/user/files')
            >>> sftp.close()
            >>> ssh.close()
        """
        try:
            # 获取文件列表
            attr_list = sftp.listdir_attr(remote_dir)
            
            # 转换为字典列表
            files = []
            for attr in attr_list:
                # 判断是否为目录
                is_dir = bool(attr.st_mode & 0o40000)  # 检查是否设置了目录标志
                
                files.append({
                    'name': attr.filename,
                    'is_dir': is_dir,
                    'size': attr.st_size,
                    'mode': attr.st_mode,
                    'atime': attr.st_atime,  # 访问时间
                    'mtime': attr.st_mtime,  # 修改时间
                })
            
            logger.info(f"列出SFTP目录成功: {remote_dir} (共 {len(files)} 个文件/目录)")
            return files
        except self.paramiko.SSHException as e:
            raise NetworkException(f"列出SFTP目录失败: {e}")
        except Exception as e:
            raise NetworkException(f"列出SFTP目录失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def download_file(self, sftp: 'paramiko.SFTPClient', remote_path: str, local_path: Union[str, Path],
                    progress_callback: Optional[Callable[[int, int], None]] = None) -> str:
        """
        从SFTP服务器下载文件
        
        Args:
            sftp: SFTP客户端
            remote_path: 远程文件路径
            local_path: 本地文件路径
            progress_callback: 进度回调函数，接收已下载大小和总大小两个参数，默认为None
            
        Returns:
            下载的文件路径
            
        Raises:
            NetworkException: 下载失败时抛出
            
        Examples:
            >>> handler = SFTPHandler()
            >>> ssh, sftp = handler.connect('sftp.example.com', user='user', password='pass')
            >>> # 下载文件
            >>> file_path = handler.download_file(sftp, '/home/user/file.zip', 'downloads/file.zip')
            >>> sftp.close()
            >>> ssh.close()
        """
        local_path = Path(local_path)
        
        # 确保输出目录存在
        local_path.parent.mkdir(parents=True, exist_ok=True)
        
        try:
            # 获取文件属性
            attr = sftp.stat(remote_path)
            total_size = attr.st_size
            
            # 如果有进度回调，使用自定义回调
            if progress_callback:
                # 创建临时文件
                with tempfile.NamedTemporaryFile(delete=False) as temp_file:
                    temp_path = temp_file.name
                
                # 下载到临时文件
                downloaded_size = [0]  # 使用列表以便在回调中修改
                
                # 定义回调函数
                def callback(bytes_transferred, total_bytes):
                    # 更新已下载大小
                    current_size = bytes_transferred
                    if current_size > downloaded_size[0]:
                        downloaded_size[0] = current_size
                        progress_callback(current_size, total_size)
                
                # 下载文件
                sftp.get(remote_path, temp_path, callback=callback)
                
                # 移动临时文件到目标位置
                import shutil
                shutil.move(temp_path, local_path)
            else:
                # 直接下载文件
                sftp.get(remote_path, str(local_path))
            
            logger.info(f"从SFTP下载文件成功: {remote_path} -> {local_path}")
            return str(local_path)
        except self.paramiko.SSHException as e:
            raise NetworkException(f"从SFTP下载文件失败: {e}")
        except Exception as e:
            raise NetworkException(f"从SFTP下载文件失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def upload_file(self, sftp: 'paramiko.SFTPClient', local_path: Union[str, Path], remote_path: str,
                  progress_callback: Optional[Callable[[int, int], None]] = None) -> None:
        """
        上传文件到SFTP服务器
        
        Args:
            sftp: SFTP客户端
            local_path: 本地文件路径
            remote_path: 远程文件路径
            progress_callback: 进度回调函数，接收已上传大小和总大小两个参数，默认为None
            
        Raises:
            NetworkException: 上传失败时抛出
            
        Examples:
            >>> handler = SFTPHandler()
            >>> ssh, sftp = handler.connect('sftp.example.com', user='user', password='pass')
            >>> # 上传文件
            >>> handler.upload_file(sftp, 'example.txt', '/home/user/example.txt')
            >>> sftp.close()
            >>> ssh.close()
        """
        local_path = Path(local_path)
        
        # 检查本地文件是否存在
        if not local_path.exists():
            raise NetworkException(f"要上传的文件不存在: {local_path}")
        
        try:
            # 获取文件大小
            total_size = local_path.stat().st_size
            
            # 如果有进度回调，使用自定义回调
            if progress_callback:
                # 上传文件
                uploaded_size = [0]  # 使用列表以便在回调中修改
                
                # 定义回调函数
                def callback(bytes_transferred, total_bytes):
                    # 更新已上传大小
                    current_size = bytes_transferred
                    if current_size > uploaded_size[0]:
                        uploaded_size[0] = current_size
                        progress_callback(current_size, total_size)
                
                # 上传文件
                sftp.put(str(local_path), remote_path, callback=callback)
            else:
                # 直接上传文件
                sftp.put(str(local_path), remote_path)
            
            logger.info(f"上传文件到SFTP成功: {local_path} -> {remote_path}")
        except self.paramiko.SSHException as e:
            raise NetworkException(f"上传文件到SFTP失败: {e}")
        except Exception as e:
            raise NetworkException(f"上传文件到SFTP失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def create_directory(self, sftp: 'paramiko.SFTPClient', remote_dir: str) -> None:
        """
        在SFTP服务器上创建目录
        
        Args:
            sftp: SFTP客户端
            remote_dir: 远程目录路径
            
        Raises:
            NetworkException: 创建失败时抛出
            
        Examples:
            >>> handler = SFTPHandler()
            >>> ssh, sftp = handler.connect('sftp.example.com', user='user', password='pass')
            >>> # 创建目录
            >>> handler.create_directory(sftp, '/home/user/new_folder')
            >>> sftp.close()
            >>> ssh.close()
        """
        try:
            # 尝试创建目录
            sftp.mkdir(remote_dir)
            
            logger.info(f"在SFTP服务器上创建目录成功: {remote_dir}")
        except IOError as e:
            # 检查是否是因为目录已存在
            if 'Failure' in str(e) and 'already exists' in str(e).lower():
                logger.warning(f"SFTP目录已存在: {remote_dir}")
            else:
                raise NetworkException(f"在SFTP服务器上创建目录失败: {e}")
        except Exception as e:
            raise NetworkException(f"在SFTP服务器上创建目录失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def delete_file(self, sftp: 'paramiko.SFTPClient', remote_path: str) -> None:
        """
        删除SFTP服务器上的文件
        
        Args:
            sftp: SFTP客户端
            remote_path: 远程文件路径
            
        Raises:
            NetworkException: 删除失败时抛出
            
        Examples:
            >>> handler = SFTPHandler()
            >>> ssh, sftp = handler.connect('sftp.example.com', user='user', password='pass')
            >>> # 删除文件
            >>> handler.delete_file(sftp, '/home/user/example.txt')
            >>> sftp.close()
            >>> ssh.close()
        """
        try:
            # 删除文件
            sftp.remove(remote_path)
            
            logger.info(f"删除SFTP服务器上的文件成功: {remote_path}")
        except self.paramiko.SSHException as e:
            raise NetworkException(f"删除SFTP服务器上的文件失败: {e}")
        except Exception as e:
            raise NetworkException(f"删除SFTP服务器上的文件失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def rename_file(self, sftp: 'paramiko.SFTPClient', from_path: str, to_path: str) -> None:
        """
        重命名SFTP服务器上的文件或目录
        
        Args:
            sftp: SFTP客户端
            from_path: 原路径
            to_path: 新路径
            
        Raises:
            NetworkException: 重命名失败时抛出
            
        Examples:
            >>> handler = SFTPHandler()
            >>> ssh, sftp = handler.connect('sftp.example.com', user='user', password='pass')
            >>> # 重命名文件
            >>> handler.rename_file(sftp, '/home/user/old.txt', '/home/user/new.txt')
            >>> sftp.close()
            >>> ssh.close()
        """
        try:
            # 重命名文件或目录
            sftp.rename(from_path, to_path)
            
            logger.info(f"重命名SFTP服务器上的文件/目录成功: {from_path} -> {to_path}")
        except self.paramiko.SSHException as e:
            raise NetworkException(f"重命名SFTP服务器上的文件/目录失败: {e}")
        except Exception as e:
            raise NetworkException(f"重命名SFTP服务器上的文件/目录失败: {e}")
    
    @exception_handler(exception_type=Exception, reraise_as=NetworkException)
    def download_directory(self, sftp: 'paramiko.SFTPClient', remote_dir: str, local_dir: Union[str, Path]) -> List[str]:
        """
        递归下载SFTP服务器上的目录
        
        Args:
            sftp: SFTP客户端
            remote_dir: 远程目录路径
            local_dir: 本地目录路径
            
        Returns:
            下载的文件路径列表
            
        Raises:
            NetworkException: 下载失败时抛出
            
        Examples:
            >>> handler = SFTPHandler()
            >>> ssh, sftp = handler.connect('sftp.example.com', user='user', password='pass')
            >>> # 下载整个目录
            >>> files = handler.download_directory(sftp, '/home/user/files', 'downloads/files')
            >>> sftp.close()
            >>> ssh.close()
        """
        local_dir = Path(local_dir)
        
        # 确保本地目录存在
        local_dir.mkdir(parents=True, exist_ok=True)
        
        try:
            # 获取文件列表
            files = self.list_files(sftp, remote_dir)
            
            # 下载文件和子目录
            downloaded_files = []
            
            for item in files:
                name = item['name']
                # 跳过当前目录和父目录
                if name in ('.', '..'):
                    continue
                
                remote_path = f"{remote_dir}/{name}"
                local_path = local_dir / name
                
                if item['is_dir']:
                    # 递归下载子目录
                    sub_files = self.download_directory(sftp, remote_path, local_path)
                    downloaded_files.extend(sub_files)
                else:
                    # 下载文件
                    downloaded_file = self.download_file(sftp, remote_path, local_path)
                    downloaded_files.append(downloaded_file)
            
            logger.info(f"从SFTP下载目录成功: {remote_dir} -> {local_dir} (共 {len(downloaded_files)} 个文件)")
            return downloaded_files
        except NetworkException:
            # 重新抛出已经处理过的异常
            raise
        except self.paramiko.SSHException as e:
            raise NetworkException(f"从SFTP下载目录失败: {e}")
        except Exception as e:
            raise NetworkException(f"从SFTP下载目录失败: {e}")